package net.aiion.weave.impl.ui;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import net.aiion.weave.impl.ui.binder.BinderException;
import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.naming.Context;
import net.aiion.weave.spi.naming.Contextual;
import net.aiion.weave.spi.naming.annotations.GlobalScoped;
import net.aiion.weave.spi.naming.annotations.Named;
import net.aiion.weave.spi.ui.Bindable;

@GlobalScoped
@Named
public class BindableFactory
{
    private static final String QUALIFIED_BINDABLE_NAME_PATTERN = "%s.%s";

    private final Map<String, Class<? extends Bindable>> bindablesByName;

    public BindableFactory() {
        bindablesByName = new HashMap<>();
        lookupBindables();
    }

    private void lookupBindables() {
        final Collection<Class<? extends Bindable>> bindableClasses = Bytecode
            .findClassesByInterfaceClass(Bindable.class);
        for (Class<? extends Bindable> bindableClass : bindableClasses) {
            lookupBindable(bindableClass);
        }
    }

    private void lookupBindable(final Class<? extends Bindable> aBindableClass) {
        final String[] bindableNames = lookupBindableNames(aBindableClass);
        for (String bindableName : bindableNames) {
            final String fullName = createFullName(aBindableClass.getPackage().getName(), bindableName);
            if (bindablesByName.containsKey(fullName)) {
                throw new BinderException(String.format(
                    "Name '%s' is used for both bindable classes '%s' and '%s', but must be unique.", fullName,
                    bindablesByName.get(fullName), aBindableClass));
            }
            bindablesByName.put(fullName, aBindableClass);
        }
    }

    private String[] lookupBindableNames(final Class<? extends Bindable> aBindableClass) {
    	// TODO Danger, Contextual and Bindable are now only Wrappers of a component, not the component itself
        if (Contextual.class.isAssignableFrom(aBindableClass)) {
            return ((Contextual) createBindableComponent(aBindableClass)).getNames();
        }

        return new String[] {
            aBindableClass.getSimpleName()
        };
    }

    public Bindable createBindable(final Context aContext, final String aBindableNamespace, final String aBindableName)
    {
        final String fullName = createFullName(aBindableNamespace, aBindableName);
        if (!bindablesByName.containsKey(fullName)) {
            throw new BinderException(String.format("Unknown bindable class for name '%s' requested.", fullName));
        }

        return aContext.lookup(bindablesByName.get(fullName));
    }

    private String createFullName(final String aNamespace, final String aName) {
        return String.format(QUALIFIED_BINDABLE_NAME_PATTERN, aNamespace, aName);
    }

    /*
     * Returns an instance of the bindable component, if it is known. Note: The component is not bound to any Context.
     */
    public Bindable createBindable(final String aBindableComponentNamespace, final String aBindableComponentName) {
        final String qualifiedBindableName = String.format(QUALIFIED_BINDABLE_NAME_PATTERN,
            aBindableComponentNamespace, aBindableComponentName);
        if (!bindablesByName.containsKey(qualifiedBindableName)) {
            throw new BinderException(String.format("Unknown bindable component class for name '%s' requested.",
                qualifiedBindableName));
        }

        return createBindableComponent(bindablesByName.get(qualifiedBindableName));
    }

    private Bindable createBindableComponent(final Class<? extends Bindable> aBindableComponentClass) {
        try {
            return aBindableComponentClass.newInstance();
        } catch (final InstantiationException | IllegalAccessException e) {
            throw new BinderException(e);
        }
    }
}
